/*
 * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pig4cloud.pig.auth.endpoint;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pig.admin.api.vo.TokenVo;
import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.core.util.RedisUtils;
import com.pig4cloud.pig.common.core.util.RetOps;
import com.pig4cloud.pig.common.core.util.SpringContextHolder;
import com.pig4cloud.pig.common.security.annotation.Inner;
import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils;
import com.pig4cloud.pig.common.security.util.OAuth2ErrorCodesExpand;
import com.pig4cloud.pig.common.security.util.OAuthClientException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * OAuth2 令牌端点控制器
 * <p>
 * 提供 OAuth2 令牌相关的核心功能：
 * 1. 授权码模式的登录和确认页面
 * 2. 令牌的检查、撤销和注销
 * 3. 令牌的分页查询和管理
 * 4. 用户登出和会话清理
 *
 * @author lengleng
 * @date 2025/05/30
 */
@Slf4j
@RestController
@RequestMapping
@RequiredArgsConstructor
public class PigTokenEndpoint {

	/**
	 * OAuth2 访问令牌响应消息转换器
	 * 用于将 OAuth2AccessTokenResponse 对象转换为 HTTP 响应
	 */
	private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();

	/**
	 * 认证失败处理器
	 * 用于统一处理认证失败的情况，返回标准错误格式
	 */
	private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler();

	/**
	 * OAuth2 授权服务
	 * 用于管理和查询 OAuth2 授权信息
	 */
	private final OAuth2AuthorizationService authorizationService;

	/**
	 * 远程客户端详情服务
	 * 用于获取 OAuth2 客户端的详细信息
	 */
	private final RemoteClientDetailsService clientDetailsService;

	/**
	 * 缓存管理器
	 * 用于管理用户信息缓存，支持快速清除用户会话
	 */
	private final CacheManager cacheManager;

	/**
	 * 授权码模式：认证页面
	 * <p>
	 * 当用户使用授权码模式访问 OAuth2 服务时，需要先登录
	 * 此方法返回登录页面，并可以显示登录错误信息
	 * 
	 * @param modelAndView 视图模型对象
	 * @param error 表单登录失败处理回调的错误信息
	 * @return 包含登录页面视图和错误信息的 ModelAndView 对象
	 */
	@GetMapping("/token/login")  // GET 请求，路径为 /token/login
	public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
		modelAndView.setViewName("ftl/login");  // 设置视图名称，使用 FreeMarker 模板
		modelAndView.addObject("error", error);  // 将错误信息传递到页面
		return modelAndView;
	}

	/**
	 * 授权码模式：授权确认页面
	 * <p>
	 * 当客户端请求用户授权时，显示授权确认页面
	 * 用户可以查看客户端信息和请求的权限范围，并决定是否授权
	 * 
	 * @param principal 当前登录的用户主体信息
	 * @param modelAndView 模型和视图对象
	 * @param clientId 客户端 ID
	 * @param scope 请求的权限范围
	 * @param state 客户端传递的状态参数，用于防止 CSRF 攻击
	 * @return 包含确认页面信息的 ModelAndView 对象
	 */
	@GetMapping("/oauth2/confirm_access")  // OAuth2 标准授权确认路径
	public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
			@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
			@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
			@RequestParam(OAuth2ParameterNames.STATE) String state) {
		// 查询客户端详情，验证 clientId 合法性
		SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId))
			.getData()
			.orElseThrow(() -> new OAuthClientException("clientId 不合法"));

		// 解析客户端支持的授权范围
		Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
		modelAndView.addObject("clientId", clientId);
		modelAndView.addObject("state", state);
		modelAndView.addObject("scopeList", authorizedScopes);  // 权限列表
		modelAndView.addObject("principalName", principal.getName());  // 当前登录用户
		modelAndView.setViewName("ftl/confirm");  // 确认页面模板
		return modelAndView;
	}

	/**
	 * 用户注销登录
	 * <p>
	 * 通过 Bearer Token 注销用户的当前会话：
	 * 1. 解析 Authorization 头中的 Bearer Token
	 * 2. 删除对应的授权信息
	 * 3. 清除用户缓存
	 * 
	 * @param authHeader 认证头信息，格式为 "Bearer {token}"
	 * @return 返回操作结果，成功返回 true
	 */
	@DeleteMapping("/token/logout")  // DELETE 请求，路径为 /token/logout
	public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
		if (StrUtil.isBlank(authHeader)) {
			return R.ok();  // 没有令牌，直接返回成功
		}

		// 从 Authorization 头中提取令牌值
		String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
		return removeToken(tokenValue);
	}

	/**
	 * 检查令牌有效性 (内置端点)
	 * <p>
	 * OAuth2 的一个核心功能，用于验证访问令牌的有效性：
	 * 1. 验证令牌是否存在
	 * 2. 验证令牌是否有效
	 * 3. 返回令牌的详细信息（如用户、权限、过期时间等）
	 * 
	 * @param token 待验证的访问令牌
	 * @param response HTTP 响应对象
	 * @param request HTTP 请求对象
	 * @throws InvalidBearerTokenException 令牌无效或缺失时抛出异常
	 */
	@SneakyThrows  // 自动处理受检异常
	@GetMapping("/token/check_token")  // OAuth2 标准的令牌检查端点
	public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);

		// 令牌为空，返回 401 未授权
		if (StrUtil.isBlank(token)) {
			httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
			this.authenticationFailureHandler.onAuthenticationFailure(request, response,
					new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
			return;
		}
		
		// 根据令牌查找授权信息
		OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);

		// 如果令牌不存在或无效，返回 401
		if (authorization == null || authorization.getAccessToken() == null) {
			this.authenticationFailureHandler.onAuthenticationFailure(request, response,
					new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
			return;
		}

		// 获取令牌的声明信息（包含用户、权限等）
		Map<String, Object> claims = authorization.getAccessToken().getClaims();
		// 构建并返回令牌响应
		OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization,
				claims);
		this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse);
	}

	/**
	 * 删除指定令牌 （内部接口）
	 * <p>
	 * 用于主动撤销令牌，通常用于：
	 * 1. 用户主动登出
	 * 2. 管理员强制踢出用户
	 * 3. 令牌异常需要清理
	 * 
	 * 处理流程：
	 * 1. 清除用户详情缓存
	 * 2. 删除访问令牌
	 * 3. 发布登出成功事件
	 * 
	 * @param token 要删除的令牌值
	 * @return 删除结果
	 */
	@Inner  // 内部接口标记，无需认证即可调用
	@DeleteMapping("/token/remove/{token}")
	public R<Boolean> removeToken(@PathVariable("token") String token) {
		// 根据令牌查找授权信息
		OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
		if (authorization == null) {
			return R.ok();  // 令牌不存在，直接返回成功
		}

		OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
		if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
			return R.ok();  // 无效令牌，直接返回成功
		}
		
		// 清空用户信息缓存（立即删除）
		cacheManager.getCache(CacheConstants.USER_DETAILS).evictIfPresent(authorization.getPrincipalName());
		// 从授权服务中删除授权信息
		authorizationService.remove(authorization);
		// 发布登出成功事件，保存登出日志
		SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
				authorization.getPrincipalName(), authorization.getRegisteredClientId())));
		return R.ok();
	}

	/**
	 * 分页查询令牌列表 （内部接口）
	 * <p>
	 * 用于管理员查看和管理系统中的所有活跃令牌：
	 * 1. 支持按用户名搜索
	 * 2. 支持分页显示
	 * 3. 显示令牌详细信息（客户端、用户、过期时间等）
	 * 
	 * @param params 请求参数，包含：
	 *               - username: 用户名（可选）
	 *               - current: 当前页码
	 *               - size: 每页数量
	 * @return 分页结果，包含令牌信息列表
	 */
	@Inner  // 内部接口标记
	@PostMapping("/token/page")
	public R<Page> tokenList(@RequestBody Map<String, Object> params) {
		// 解析请求参数
		String username = MapUtil.getStr(params, SecurityConstants.USERNAME);
		String pattern = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS);  // Redis 键模式
		int current = MapUtil.getInt(params, CommonConstants.CURRENT);
		int size = MapUtil.getInt(params, CommonConstants.SIZE);
		Page result = new Page(current, size);

		// 获取所有令牌键的总数
		List<String> allKeys = RedisUtils.scan(pattern);
		result.setTotal(allKeys.size());

		// 根据分页参数获取当前页的键
		List<String> pageKeys = RedisUtils.findKeysForPage(pattern, current - 1, size);
		// 批量获取授权信息
		List<OAuth2Authorization> pagedAuthorizations = RedisUtils.multiGet(pageKeys);

		// 转换为 TokenVo 并过滤
		List<TokenVo> tokenVoList = pagedAuthorizations.stream()
			.filter(Objects::nonNull)
			.map(this::convertToTokenVo)  // 转换为前端展示对象
			.filter(tokenVo -> {
				if (StrUtil.isBlank(username)) {
					return true;  // 没有用户名过滤条件，返回所有
				}
				// 按用户名前缀过滤
				return StrUtil.startWithAnyIgnoreCase(tokenVo.getUsername(), username);
			})
			.toList();

		// 如果有用户名过滤，更新总数
		if (StrUtil.isNotBlank(username)) {
			result.setTotal(tokenVoList.size());
		}

		result.setRecords(tokenVoList);
		return R.ok(result);
	}

	/**
	 * 将 OAuth2Authorization 转换为 TokenVo
	 * <p>
	 * 提取授权信息中的关键数据，转换为前端展示对象：
	 * - 客户端 ID
	 * - 用户名
	 * - 令牌值
	 * - 颁发时间和过期时间
	 * 
	 * @param authorization OAuth2 授权对象
	 * @return TokenVo 前端展示对象
	 */
	private TokenVo convertToTokenVo(OAuth2Authorization authorization) {
		TokenVo tokenVo = new TokenVo();
		tokenVo.setClientId(authorization.getRegisteredClientId());  // 客户端 ID
		tokenVo.setId(authorization.getId());  // 授权 ID
		tokenVo.setUsername(authorization.getPrincipalName());  // 用户名
		
		// 获取访问令牌信息
		OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
		tokenVo.setAccessToken(accessToken.getToken().getTokenValue());

		// 格式化过期时间
		String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
				DatePattern.NORM_DATETIME_PATTERN);
		tokenVo.setExpiresAt(expiresAt);

		// 格式化颁发时间
		String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
				DatePattern.NORM_DATETIME_PATTERN);
		tokenVo.setIssuedAt(issuedAt);
		return tokenVo;
	}

}
